from requests.packages.urllib3.exceptions import InsecureRequestWarning
from threading import Lock, Thread
from tqdm import tqdm
import traceback
import requests
import time
import copy
import os
import re


requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


class Downloader:
    
    def __init__(self, url, filename, cache_dir, headers=None, thread_num=8, strict=True):
        self.url = url
        self.filename = filename
        self.headers = headers
        self.cache_dir = cache_dir
        if headers is None:
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
                'Connection': 'keep-alive'
            }
        self.strict = strict

        self.cacheFileName = self.cache_dir + self.validateTitle(self.filename) + '.cache'
        self.file = None
        self.file_write_lock = Lock()
        self.thread_num = thread_num
        self.one_block_size = 1024 * 64
        self.session = requests.Session()

        self.planned_size = 0
        self.planned_size_lock = Lock()

        self.downloading_size = 0
        self.downloading_size_lock = Lock()

        self.downloaded_size = 0

    def validateTitle(self, title):
        rstr = r"[/\\\:\*\?\"\<\>\|]"  # / \ : * ? " < > |
        new_title = re.sub(rstr, "_", title)
        return new_title

    def download(self, file_size):
        while self.planned_size < file_size:
            with self.planned_size_lock:
                start = self.planned_size
                end = min(self.planned_size + self.one_block_size, file_size)
                self.planned_size = end
            if start == end:
                break

            headers = copy.deepcopy(self.headers)
            headers['Range'] = f'bytes={start}-{end}'
            while True:
                try:
                    r = self.session.get(
                        self.url, stream=True, headers=headers, timeout=1)
                    with self.file_write_lock:
                        self.file.seek(start)
                        self.file.write(r.content)
                    break
                except requests.exceptions.ReadTimeout:
                    pass
                except requests.exceptions.ConnectionError:
                    time.sleep(0.1)
                    pass
                except requests.exceptions.ChunkedEncodingError:
                    pass
                except:
                    print(traceback.format_exc())
            with self.downloading_size_lock:
                self.downloading_size += end - start + 1

    def get_unit(self, file_size):
        unit_str = 'B'
        unit_int = 1
        if file_size >= 1024:
            file_size /= 1024
            unit_str = 'KB'
            unit_int = 1024
        if file_size >= 1024:
            file_size /= 1024
            unit_str = 'MB'
            unit_int = 1024 * 1024
        if file_size >= 1024:
            file_size /= 1024
            unit_str = 'GB'
            unit_int = 1024 * 1024 * 1024
        return unit_int, unit_str

    def monitor_thread(self, file_size):
        file_unit_int, file_unit_str = self.get_unit(file_size)
        with tqdm(total=file_size) as pbar:
            pbar.set_postfix(总大小='%.2f %s' % (
                file_size / file_unit_int, file_unit_str), 速度='0 KB/s')
            while self.downloaded_size < file_size:
                time.sleep(1)
                with self.downloading_size_lock:
                    self.downloaded_size += self.downloading_size
                    pbar.update(self.downloading_size)
                    downloading_unit_int, downloading_unit_str = self.get_unit(
                        self.downloading_size)
                    pbar.set_postfix(总大小='%.2f %s' % (
                        file_size / file_unit_int, file_unit_str), 速度='%.2f %s/s' % (self.downloading_size / downloading_unit_int, downloading_unit_str))
                    self.downloading_size = 0

    def download_payload(self):
        r2 = requests.get(self.url, stream=True, headers=self.headers)

        size = int(r2.headers['Content-Length'])


        time_beg = time.time()
        for i in range(self.thread_num):
            t1 = Thread(target=self.download, args=(size, ))
            t1.setDaemon(True)
            t1.start()

        self.monitor_thread(size)

        time_end = time.time()
        self.file.close()
        spendTime = time_end - time_beg

        return spendTime, size

    def main(self):
        if os.path.exists(self.filename):
            if self.strict:
                print(f'[{self.filename}]已存在，即将重新下载！')
                os.remove(self.filename)
            else:
                print(f'[{self.filename}]已存在，即将跳过下载！')
                return True
        self.file = open(self.cacheFileName, 'wb')
        try:
            spendTime, size = self.download_payload()
            speed_unit_int, speed_unit_str = self.get_unit(size / spendTime)
            os.rename(self.cacheFileName, self.filename)
            print(f'{self.filename}下载完成！用时 %.2f s 平均速度：%.2f %s/s' %
                  (spendTime, size / spendTime / speed_unit_int, speed_unit_str))
        except:
            print(f'{self.filename}下载出错！')
            print(traceback.format_exc())
            return False
        return True


class Bilibili:
    def __init__(self, url,video_dir,cache_dir):
        self.url = url
        self.cache_dir = cache_dir
        self.video_dir = video_dir
        self.headers = {
            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "accept-encoding": "gzip, deflate, br",
            "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
            "cache-control": "max-age=0",
            "referer": "https://www.bilibili.com/",
            "sec-fetch-dest": "document",
            "sec-fetch-mode": "navigate",
            "sec-fetch-site": "same-origin",
            "sec-fetch-user": "?1",
            "upgrade-insecure-requests": "1",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
        }
        self.Str = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'  # 准备的一串指定字符串
        self.s = [11, 10, 3, 8, 4, 6, 2, 9, 5, 7]
        self.xor = 177451812
        self.add = 100618342136696320
        if not os.path.exists(self.video_dir): os.mkdir(self.video_dir)

    def getaid(self, bid):
        url = rf'https://www.bilibili.com/video/{bid}'
        a = requests.get(url, headers=self.headers)
        if bid[:2] == 'av':
            aid = bid[2:]
        else:
            aid = re.findall(r'"' + bid + r'":\{"aid":(.*?),', a.text)[0]
        title = re.findall(
            r'<h1 [^<>]*?title="(.*?)" class="video-title', a.text)[0]
        return aid, title
    
    def getbid(self, aid):
        if aid[:2] == 'av':
            aid = int(aid[2:])
            table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
            tr = {}
            for i in range(58):
                tr[table[i]] = i
            s = [11, 10, 3, 8, 4, 6]
            xor = 177451812
            add = 8728348608
            x = (aid ^ xor) + add
            r = list('BV1  4 1 7  ')
            for i in range(6):
                r[s[i]] = table[x // 58 ** i % 58]
            return ''.join(r)
        else:
            return aid

    def getcid(self, bid):
        url = 'https://api.bilibili.com/x/player/pagelist?bvid=' + bid
        response = requests.get(url, headers=self.headers)
        a = response.json()
        with open('json.json', 'w', encoding='utf8') as f:
            f.write(str(a))
        ans = []

        data = a['data']
        cids = data
        for i in cids:
            cid = i['cid']
            title = i['part']
            ans.append({'bid': bid, 'cid': cid, 'title': title})
        return ans

    def getVideoList(self, bid, cid, quality):
        url_api = f'https://api.bilibili.com/x/player/playurl?cid={cid}&bvid={bid}&qn={quality}'
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
            'Host': 'api.bilibili.com'
        }
        a = requests.get(url_api, headers=headers)
        a = a.json()
        video_list = []
        for i in a['data']['durl']:
            video_list.append(i['url'])
        return video_list

    def validateTitle(self, title):
        rstr = r"[/\\\:\*\?\"\<\>\|]"  # '\ : * ? " < > |'
        new_title = re.sub(rstr, "_", title)  # 替换为下划线
        return new_title

    def download(self, url, filename, bid):
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
            'Accept': '*/*',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate, br',
            'Range': 'bytes=0-',
            'Referer': 'https://www.bilibili.com/video/' + bid,
            'Origin': 'https://www.bilibili.com',
            'Connection': 'keep-alive',
        }
        downloader = Downloader(
            url, filename, self.cache_dir,headers=headers, strict=False)
        downloader.main()

    def run(self):
        self.bid = self.getbid(self.bid)
        cid = self.getcid(self.bid)
        aid, title = self.getaid(self.bid)

        title = self.validateTitle(title)
        if len(cid) > 1:
            video_path = os.path.join(self.video_dir,title)
            if not os.path.exists(video_path):os.mkdir(video_path)
            for ind, c in enumerate(cid):
                urls = self.getVideoList(self.bid, c['cid'], '80')

                Prefix = 0
                if len(cid) < 10:
                    Prefix = '%d-'
                elif len(cid) < 100:
                    Prefix = '%02d-'
                elif len(cid) < 1000:
                    Prefix = '%03d-'
                elif len(cid) < 10000:
                    Prefix = '%04d-'

                fileName = Prefix % (ind + 1) + self.validateTitle(c['title'])
                print(f'P{ind + 1}/{len(cid)} title: {fileName}')
                fileName = os.path.join(self.video_dir,title,f"{fileName}.flv")
                url = urls[0]
                self.download(url, fileName, self.bid)
                return fileName 
        elif len(cid) == 1:
            urls = self.getVideoList(self.bid, cid[0]['cid'], '80')
            print('title', title)
            fileName = os.path.join(self.video_dir,f"{title}.flv")
            url = urls[0]
            self.download(url, fileName, self.bid)
            return fileName
        else:
            print('未解析到视频数据！')

    def main(self):
        try:
            self.bid = re.findall(r'video/([^?/]+)', self.url)[0]
        except:
            print('不支持的类型！')
            return
   
        return self.run()


